

package com.googlecode.flac4j.format;


import com.googlecode.flac4j.format.frame.Frame;
import com.googlecode.flac4j.format.metadata.StreamInfo;
import com.googlecode.flac4j.format.metadata.Block;
import com.googlecode.flac4j.format.metadata.Header;
import com.googlecode.flac4j.format.metadata.BlockType;

import com.googlecode.jinahya.io.BitInput;
import com.googlecode.jinahya.io.BitOutput;
import com.googlecode.jinahya.util.BitAccessible;

import java.io.EOFException;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;


/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 */
@XmlRootElement
@XmlType(propOrder = {"blocks", "frames"})
public class Stream implements BitAccessible {


    /** GENERATED. */
    private static final long serialVersionUID = 4965040490873552318L;


    /** Stream marker. "fLaC". */
    public static final int MARKER = 0x66 << 24 | 0x4C << 16 | 0x61 << 8 | 0x43;


    @Override
    public void read(final BitInput input) throws IOException {

        assert (input.readInt(32) == MARKER);

        blocks.clear();
        while (true) {
            final Block block = new Block();
            block.setStream(this);
            block.read(input);
            addBlock(block);
            if (block.getHeader().isLastBlock()) {
                break;
            }
        }

        frames.clear();
        while (true) {
            try {
                final Frame frame = new Frame();
                frame.read(input);
                frames.add(frame);
            } catch (EOFException eofe) {
                break;
            }
        }
    }


    @Override
    public void write(final BitOutput output) throws IOException {

        output.writeInt(32, MARKER);

        if (blocks.isEmpty()) {
            throw new IllegalStateException("empty blocks");
        }

        if (getStreamInfoBlock() == null) {
            throw new IllegalArgumentException("no STREAMINOF block");
        }

        blocks.get(blocks.size() - 1).getHeader().setLastBlock(true);

        for (Block block : blocks) {
            block.write(output);
        }

        for (Frame frame : frames) {
            frame.write(output);
        }
    }


    /**
     * Returns the block for STREAMINFO if exist.
     *
     * @return STREAMINFO block or null if not found
     */
    public final Block getStreamInfoBlock() {

        final Block firstBlock = blocks.get(0);
        final Header firstBlockHeader = firstBlock.getHeader();
        if (firstBlockHeader.getBlockType().equals(BlockType.STREAMINFO)) {
            return firstBlock;
        }

        return null;
    }


    /**
     * Returns the data of the STREAMINFO block if exist.
     *
     * @return STREAMINFO data block or null if not found
     */
    public final StreamInfo getStreamInfoBlockData() {

        final Block streamInfoBlock = getStreamInfoBlock();
        if (streamInfoBlock == null) {
            return null;
        }

        return (StreamInfo) streamInfoBlock.getData();
    }


    /**
     * Returns the size of blocks.
     *
     * @return blockSize
     */
    public int getBlockCount() {
        return blocks.size();
    }


    /**
     * Clears blocks.
     */
    public void clearBlocks() {
        blocks.clear();
    }


    /**
     * Adds given <code>block</code>.
     *
     * @param block block to add
     */
    public void addBlock(final Block block) {

        if (block == null) {
            throw new NullPointerException("null block");
        }

        block.setStream(this);

        if (block.getHeader().getBlockType().equals(BlockType.STREAMINFO)) {

            // remove STREAMINFO block if exist
            if (!blocks.isEmpty()) {
                final Block firstBlock = blocks.get(0);
                final Header firstBlockHeader = firstBlock.getHeader();
                if (firstBlockHeader.getBlockType().equals(
                    BlockType.STREAMINFO)) {
                    blocks.remove(0);
                }
            }

            blocks.add(0, block);
            return;
        }

        final Block lastBlock = blocks.get(blocks.size() - 1);
        final Header lastBlockHeader = lastBlock.getHeader();
        if (lastBlockHeader.isLastBlock()) {
            lastBlockHeader.setLastBlock(false);
        }

        block.setStream(this);

        blocks.add(block);
    }


    /**
     * Returns an unmodifiable list of blocks.
     *
     * @return blocks.
     */
    public List<Block> getBlocks() {
        return Collections.unmodifiableList(blocks);
    }


    /** blocks. */
    @XmlElement(name = "block")
    @XmlElementWrapper(required = true)
    private final List<Block> blocks = new ArrayList<Block>();


    /** frames. */
    @XmlElement(name = "frame")
    @XmlElementWrapper(required = true)
    private final List<Frame> frames = new ArrayList<Frame>();


}

